﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.ServiceModel.Web;
using System.Text;
using System.Threading.Tasks;
using Restful.Net.Reflection;
using Restful.Net.Serialization;

namespace Restful.Net
{
    public class RestRouter
    {
        private readonly IServiceFactory handlerFactory;
        private readonly List<ServiceTypeMap> maps = new List<ServiceTypeMap>();

        private readonly ContentTypeNegotiator contentNegotiator;
        private readonly IUserAuthenticator authenticator;
        private static RestRouter instance;

        private readonly List<ISerializer> serializers = new List<ISerializer>();

        private Uri baseUri;

        internal RestRouter(
            IServiceFactory handlerFactory,
            Uri baseUri,
            ContentTypeNegotiator contentNegotiator,
            IUserAuthenticator authenticator)
        {
            this.handlerFactory = handlerFactory;
            this.baseUri = baseUri;
            this.contentNegotiator = contentNegotiator;
            this.authenticator = authenticator;
            serializers.Add(new JsonSerializer());
            serializers.Add(new XmlSerializer());
        }

        public static RestRouter CreateInstance(
            IServiceFactory handlerFactory,
            Uri baseUri = null,
            IUserAuthenticator authenticator = null)
        {
            if (authenticator != null)
            {
                instance = new RestRouter(handlerFactory, baseUri, new ContentTypeNegotiator(), authenticator);
            }
            else
            {
                instance = new RestRouter(handlerFactory, baseUri, new ContentTypeNegotiator(), new AnonymousAuthenticator());
            }

            return instance;
        }

        public static RestRouter Instance
        {
            get
            {
                return instance;
            }
        }

        public List<ISerializer> Serializers
        {
            get
            {
                return serializers;
            }
        }

        public void Add<THandler>(string route)
        {
            ServiceTypeMap map = new ServiceTypeMap(typeof(THandler), route);
            maps.Add(map);
        }

        private async Task<bool> Authenticate(IRestContext context)
        {
            string authHeader = context.Request.RequestBase.Headers["Authorization"];
            if (string.IsNullOrWhiteSpace(authHeader))
            {
                // No auth provided
                throw new WebFaultException<string>("Authorization header not provided", HttpStatusCode.Unauthorized);
            }

            string[] authParts = authHeader.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

            if (authParts.Length != 2)
            {
                throw new WebFaultException<string>("Invalid authorization header provided", HttpStatusCode.BadRequest);
            }

            if (authParts[0].ToLower().Trim() != "basic")
            {
                throw new WebFaultException<string>("Invalid authorization header provided, only basic authorization is supported", HttpStatusCode.NotImplemented);
            }

            try
            {
                string[] userParts = Encoding.UTF8.GetString(Convert.FromBase64String(authParts[1]))
                                             .Split(new[] { ':' }, StringSplitOptions.None);


                if (userParts.Length != 2)
                {
                    throw new WebFaultException<string>("Invalid username or password", HttpStatusCode.Unauthorized);
                }

                try
                {
                    IUser user = await authenticator.AuthenticateAsync(userParts[0].Trim(), userParts[1].Trim());

                    if (user == null)
                    {
                        throw new WebFaultException<string>("Invalid username or password", HttpStatusCode.Unauthorized);
                    }

                    if (user.IsAuthenticated)
                    {
                        context.CurrentUser = user;
                        return true;
                    }

                    throw new WebFaultException<string>("Invalid username or password", HttpStatusCode.Unauthorized);
                }
                catch (Exception e)
                {
                    throw new WebFaultException<string>("Invalid username or password:" + e.Message, HttpStatusCode.Unauthorized);
                }
            }
            catch (FormatException e)
            {
                throw new WebFaultException<string>("Invalid authorization header provided:" + e.Message, HttpStatusCode.BadRequest);
            }
        }

        private static Uri MakeBaseUri(Uri incomingUri)
        {
            string host = incomingUri.Host;
            string scheme = incomingUri.Scheme;
            int port = incomingUri.Port;
            switch (scheme)
            {
                case "http":
                    if (port != 80)
                    {
                        return new Uri(string.Format("{0}://{1}:{2}", scheme, host, port));
                    }

                    return new Uri(string.Format("{0}://{1}", scheme, host));
                case "https":
                    if (port != 443)
                    {
                        return new Uri(string.Format("{0}://{1}:{2}", scheme, host, port));
                    }

                    return new Uri(string.Format("{0}://{1}", scheme, host));
            }

            return new Uri(string.Format("{0}://{1}:{2}", scheme, host, port));
        }

        public async Task InvokeAsync(IRestContext context)
        {
            Uri localBaseUri = baseUri;

            if (localBaseUri == null)
            {
                baseUri = MakeBaseUri(context.Request.Uri);

                localBaseUri = baseUri;
            }

            foreach (ServiceTypeMap serviceTypeMap in maps)
            {
                MethodMatch match = serviceTypeMap.GetMethod(localBaseUri, context.Request.Uri, context.Request.Verb);
                if (match == null)
                {
                    continue;
                }

                if (match.RestMethod.RequiresAuthentication)
                {
                    WebFaultException<string> authError = null;
                    try
                    {
                        await Authenticate(context);

                    }
                    catch (Exception e)
                    {
                        authError = (WebFaultException<string>)e;
                    }

                    if (authError != null)
                    {
                        await HandleErrorAsync(context, authError.Message, (int)authError.StatusCode, authError.Detail);
                        return;
                    }
                }

                context.Request.RestVariables.Add(match.Match.BoundVariables);
                context.Request.RestVariables.Add(match.Match.QueryParameters);

                object serviceInstance = handlerFactory.GetInstance(match.RestMethod.DeclaringType);
                ServiceState serviceState = new ServiceState
                {
                    Service = serviceInstance,
                    Match = match,
                    Context = context
                };

                Exception invokeError;

                try
                {
                    SetHeadersDependingOnVerb(context);

                    object invokeResult;
                    if (match.RestMethod.IsAsync)
                    {
                        invokeResult = await
                                       Task.Factory.FromAsync(
                                           (cb, st) =>
                                           match.RestMethod.BeginInvoke(serviceState, context, cb, st),
                                           ar => match.RestMethod.EndInvoke(serviceState, ar), null);
                    }
                    else
                    {
                        invokeResult = await Task.Factory.StartNew(() => match.RestMethod.Invoke(serviceInstance, context));
                    }

                    if (invokeResult == null)
                    {
                        if (!match.RestMethod.IsVoid)
                        {
                            // Method did not return any data, so translate reply into a 404.
                            await HandleErrorAsync(context, "Not found", (int)HttpStatusCode.NotFound, "No data was returned, this might not be an error");
                        }

                        return;
                    }

                    if (invokeResult.GetType().IsAssignableFrom(typeof(void)))
                    {
                        return;
                    }
                    await SetBodyOfResponseAsync(context, invokeResult);
                    return;


                }
                catch (Exception e)
                {
                    invokeError = e;
                }

                WebFaultException<string> wfes = invokeError as WebFaultException<string>;
                WebFaultException wfe = invokeError as WebFaultException;

                if (wfes != null)
                {
                    await HandleErrorAsync(context, wfes.Message, (int)wfes.StatusCode, wfes.Detail);
                }
                else if (wfe != null)
                {
                    await HandleErrorAsync(context, wfe.Message, (int)wfe.StatusCode);
                }

                await HandleErrorAsync(context, invokeError.Message);
            }

            await HandleErrorAsync(context, "Not found", (int)HttpStatusCode.NotFound, "No route found matching request");
        }

        private async Task SetBodyOfResponseAsync(IRestContext context, object invokeResult)
        {
            ISerializer defaultSerializer = new JsonSerializer();
            IList<Accept> orderedList = contentNegotiator.GetOrderedList(context.Request.RequestBase.AcceptTypes);

            ISerializer serializer = defaultSerializer;


            foreach (Accept a in orderedList)
            {
                serializer = Serializers.FirstOrDefault(se => se.Supports(a.MediaType));
                if (serializer == null)
                {
                    continue;
                }

                break;
            }

            // Return a 406, since we cannot satisfy the client
            if (serializer == null)
            {
                await HandleErrorAsync(context, "Not acceptable", (int)HttpStatusCode.NotAcceptable, "No serializers found to satify request");
                return;
            }

            context.Response.ContentType = serializer.ResponseContentType;
            context.Response.ContentEncoding = Encoding.UTF8;

            await serializer.SerializeAsync(invokeResult, context.Response.OutputStream);
        }

        private static void SetHeadersDependingOnVerb(IRestContext context)
        {
            int statusCode;
            switch (context.Request.Verb)
            {
                case HttpVerb.Delete:
                    statusCode = 200;
                    break;
                case HttpVerb.Patch:
                    statusCode = 200;
                    break;
                case HttpVerb.Post:
                    statusCode = 201;
                    break;
                case HttpVerb.Put:
                    statusCode = 200;
                    break;
                case HttpVerb.Head:
                    statusCode = 200;
                    break;
                default:

                    return;
            }

            context.Response.StatusCode = statusCode;
        }

        private static async Task HandleErrorAsync(IRestContext context, string description, int statusCode = 500, string bodyContent = null)
        {
            context.Response.StatusCode = statusCode;
            context.Response.StatusDescription = description;
            context.Response.ContentType = "text/plain";

            await context.Response.OutputStream.FlushAsync();
            if (string.IsNullOrEmpty(bodyContent))
            {
                return;
            }


            byte[] bytes = Encoding.UTF8.GetBytes(bodyContent);
            await context.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
        }
    }

    public class ServiceState
    {
        public MethodMatch Match
        {
            get;
            set;
        }

        public object Service
        {
            get;
            set;
        }

        public IRestContext Context
        {
            get;
            set;
        }
    }
}
